From 043b6b32f6046ca14500f89f09921eba755d828b Mon Sep 17 00:00:00 2001 From: Kaldari Date: Wed, 13 Feb 2013 18:05:13 -0800 Subject: [PATCH] Adding new subclass to HTMLForm for constructing a checkbox matrix Also adding corresponding support for using them within preferences Change-Id: Ie6e77dfd8edaff212655d0be1d048a10eeba341f --- RELEASE-NOTES-1.21 | 2 + includes/AutoLoader.php | 1 + includes/HTMLForm.php | 165 +++++++++++++++++++++++++++++++++++++++ includes/Preferences.php | 39 ++++++++- includes/User.php | 35 +++++++-- skins/common/shared.css | 8 ++ 6 files changed, 241 insertions(+), 9 deletions(-) diff --git a/RELEASE-NOTES-1.21 b/RELEASE-NOTES-1.21 index 30a73b0e57..2353820fc2 100644 --- a/RELEASE-NOTES-1.21 +++ b/RELEASE-NOTES-1.21 @@ -96,6 +96,8 @@ production. * (bug 5346) Categories that are redirects will be displayed italic in the category links section at the bottom of a page. * (bug 43915) New maintenance script deleteEqualMessages.php. +* You can now create checkbox option matrixes through the HTMLCheckMatrix + subclass in HTMLForm. * WikiText now permits the use of WAI-ARIA's role="presentation" inside of html elements and tables. This allows presentational markup, especially tables. To be marked up as such. diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 5bbfd96e28..f4f4cb319d 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -119,6 +119,7 @@ $wgAutoloadLocalClasses = array( 'Html' => 'includes/Html.php', 'HTMLApiField' => 'includes/HTMLForm.php', 'HTMLCheckField' => 'includes/HTMLForm.php', + 'HTMLCheckMatrix' => 'includes/HTMLForm.php', 'HTMLEditTools' => 'includes/HTMLForm.php', 'HTMLFloatField' => 'includes/HTMLForm.php', 'HTMLForm' => 'includes/HTMLForm.php', diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php index 86eb38dda1..684866fc48 100644 --- a/includes/HTMLForm.php +++ b/includes/HTMLForm.php @@ -112,6 +112,7 @@ class HTMLForm extends ContextSource { 'submit' => 'HTMLSubmitField', 'hidden' => 'HTMLHiddenField', 'edittools' => 'HTMLEditTools', + 'checkmatrix' => 'HTMLCheckMatrix', // HTMLTextField will output the correct type="" attribute automagically. // There are about four zillion other HTML5 input types, like url, but @@ -1781,6 +1782,170 @@ class HTMLCheckField extends HTMLFormField { } } +/** + * A checkbox matrix + * Operates similarly to HTMLMultiSelectField, but instead of using an array of + * options, uses an array of rows and an array of columns to dynamically + * construct a matrix of options. + */ +class HTMLCheckMatrix extends HTMLFormField { + + function validate( $value, $alldata ) { + $rows = $this->mParams['rows']; + $columns = $this->mParams['columns']; + + // Make sure user-defined validation callback is run + $p = parent::validate( $value, $alldata ); + if ( $p !== true ) { + return $p; + } + + // Make sure submitted value is an array + if ( !is_array( $value ) ) { + return false; + } + + // If all options are valid, array_intersect of the valid options + // and the provided options will return the provided options. + $validOptions = array(); + foreach ( $rows as $rowTag ) { + foreach ( $columns as $columnTag ) { + $validOptions[] = $columnTag . '-' . $rowTag; + } + } + $validValues = array_intersect( $value, $validOptions ); + if ( count( $validValues ) == count( $value ) ) { + return true; + } else { + return $this->msg( 'htmlform-select-badoption' )->parse(); + } + } + + /** + * Build a table containing a matrix of checkbox options. + * The value of each option is a combination of the row tag and column tag. + * mParams['rows'] is an array with row labels as keys and row tags as values. + * mParams['columns'] is an array with column labels as keys and column tags as values. + * @param $value Array of the options that should be checked + * @return String + */ + function getInputHTML( $value ) { + $html = ''; + $tableContents = ''; + $attribs = array(); + $rows = $this->mParams['rows']; + $columns = $this->mParams['columns']; + + // If the disabled param is set, disable all the options + if ( !empty( $this->mParams['disabled'] ) ) { + $attribs['disabled'] = 'disabled'; + } + + // Build the column headers + $headerContents = Html::rawElement( 'td', array(), ' ' ); + foreach ( $columns as $columnLabel => $columnTag ) { + $headerContents .= Html::rawElement( 'td', array(), $columnLabel ); + } + $tableContents .= Html::rawElement( 'tr', array(), "\n$headerContents\n" ); + + // Build the options matrix + foreach ( $rows as $rowLabel => $rowTag ) { + $rowContents = Html::rawElement( 'td', array(), $rowLabel ); + foreach ( $columns as $columnTag ) { + // Knock out any options that are not wanted + if ( isset( $this->mParams['remove-options'] ) + && in_array( "$columnTag-$rowTag", $this->mParams['remove-options'] ) ) + { + $rowContents .= Html::rawElement( 'td', array(), ' ' ); + } else { + // Construct the checkbox + $thisAttribs = array( + 'id' => "{$this->mID}-$columnTag-$rowTag", + 'value' => $columnTag . '-' . $rowTag + ); + $checkbox = Xml::check( + $this->mName . '[]', + in_array( $columnTag . '-' . $rowTag, (array)$value, true ), + $attribs + $thisAttribs ); + $rowContents .= Html::rawElement( 'td', array(), $checkbox ); + } + } + $tableContents .= Html::rawElement( 'tr', array(), "\n$rowContents\n" ); + } + + // Put it all in a table + $html .= Html::rawElement( 'table', array( 'class' => 'mw-htmlform-matrix' ), + Html::rawElement( 'tbody', array(), "\n$tableContents\n" ) ) . "\n"; + + return $html; + } + + /** + * Get the complete table row for the input, including help text, + * labels, and whatever. + * We override this function since the label should always be on a separate + * line above the options in the case of a checkbox matrix, i.e. it's always + * a "vertical-label". + * @param $value String the value to set the input to + * @return String complete HTML table row + */ + function getTableRow( $value ) { + list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); + $inputHtml = $this->getInputHTML( $value ); + $fieldType = get_class( $this ); + $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() ); + $cellAttributes = array( 'colspan' => 2 ); + + $label = $this->getLabelHtml( $cellAttributes ); + + $field = Html::rawElement( + 'td', + array( 'class' => 'mw-input' ) + $cellAttributes, + $inputHtml . "\n$errors" + ); + + $html = Html::rawElement( 'tr', + array( 'class' => 'mw-htmlform-vertical-label' ), $label ); + $html .= Html::rawElement( 'tr', + array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ), + $field ); + + return $html . $helptext; + } + + /** + * @param $request WebRequest + * @return Array + */ + function loadDataFromRequest( $request ) { + if ( $this->mParent->getMethod() == 'post' ) { + if ( $request->wasPosted() ) { + // Checkboxes are not added to the request arrays if they're not checked, + // so it's perfectly possible for there not to be an entry at all + return $request->getArray( $this->mName, array() ); + } else { + // That's ok, the user has not yet submitted the form, so show the defaults + return $this->getDefault(); + } + } else { + // This is the impossible case: if we look at $_GET and see no data for our + // field, is it because the user has not yet submitted the form, or that they + // have submitted it with all the options unchecked. We will have to assume the + // latter, which basically means that you can't specify 'positive' defaults + // for GET forms. + return $request->getArray( $this->mName, array() ); + } + } + + function getDefault() { + if ( isset( $this->mDefault ) ) { + return $this->mDefault; + } else { + return array(); + } + } +} + /** * A select dropdown field. Basically a wrapper for Xmlselect class */ diff --git a/includes/Preferences.php b/includes/Preferences.php index 76e1760b40..f35d754063 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -129,7 +129,7 @@ class Preferences { static function getOptionFromUser( $name, $info, $user ) { $val = $user->getOption( $name ); - // Handling for array-type preferences + // Handling for multiselect preferences if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) || ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) { $options = HTMLFormField::flattenOptions( $info['options'] ); @@ -143,6 +143,23 @@ class Preferences { } } + // Handling for checkmatrix preferences + if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) || + ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) { + $columns = HTMLFormField::flattenOptions( $info['columns'] ); + $rows = HTMLFormField::flattenOptions( $info['rows'] ); + $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name; + $val = array(); + + foreach ( $columns as $column ) { + foreach ( $rows as $row ) { + if ( $user->getOption( "$prefix-$column-$row" ) ) { + $val[] = "$column-$row"; + } + } + } + } + return $val; } @@ -1560,10 +1577,11 @@ class PreferencesForm extends HTMLForm { * @return array */ function filterDataForSubmit( $data ) { - // Support for separating MultiSelect preferences into multiple preferences + // Support for separating multi-option preferences into multiple preferences // Due to lack of array support. foreach ( $this->mFlatFields as $fieldname => $field ) { $info = $field->mParams; + if ( $field instanceof HTMLMultiSelectField ) { $options = HTMLFormField::flattenOptions( $info['options'] ); $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname; @@ -1572,6 +1590,23 @@ class PreferencesForm extends HTMLForm { $data["$prefix$opt"] = in_array( $opt, $data[$fieldname] ); } + unset( $data[$fieldname] ); + + } elseif ( $field instanceof HTMLCheckMatrix ) { + $columns = HTMLFormField::flattenOptions( $info['columns'] ); + $rows = HTMLFormField::flattenOptions( $info['rows'] ); + $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname; + foreach ( $columns as $column ) { + foreach ( $rows as $row ) { + // Make sure option hasn't been removed + if ( !isset( $info['remove-options'] ) + || !in_array( "$column-$row", $info['remove-options'] ) ) + { + $data["$prefix-$column-$row"] = in_array( "$column-$row", $data[$fieldname] ); + } + } + } + unset( $data[$fieldname] ); } } diff --git a/includes/User.php b/includes/User.php index c9b8964df2..03b11735f1 100644 --- a/includes/User.php +++ b/includes/User.php @@ -2319,6 +2319,7 @@ class User { * - 'registered' - preferences which are registered in core MediaWiki or * by extensions using the UserGetDefaultOptions hook. * - 'registered-multiselect' - as above, using the 'multiselect' type. + * - 'registered-checkmatrix' - as above, using the 'checkmatrix' type. * - 'userjs' - preferences with names starting with 'userjs-', intended to * be used by user scripts. * - 'unused' - preferences about which MediaWiki doesn't know anything. @@ -2335,6 +2336,7 @@ class User { return array( 'registered', 'registered-multiselect', + 'registered-checkmatrix', 'userjs', 'unused' ); @@ -2360,8 +2362,8 @@ class User { $prefs = Preferences::getPreferences( $this, $context ); $mapping = array(); - // Multiselect options are stored in the database with one key per - // option, each having a boolean value. Extract those keys. + // Multiselect and checkmatrix options are stored in the database with + // one key per option, each having a boolean value. Extract those keys. $multiselectOptions = array(); foreach ( $prefs as $name => $info ) { if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) || @@ -2376,6 +2378,23 @@ class User { unset( $prefs[$name] ); } } + $checkmatrixOptions = array(); + foreach ( $prefs as $name => $info ) { + if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) || + ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) { + $columns = HTMLFormField::flattenOptions( $info['columns'] ); + $rows = HTMLFormField::flattenOptions( $info['rows'] ); + $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name; + + foreach ( $columns as $column ) { + foreach ( $rows as $row ) { + $checkmatrixOptions["$prefix-$column-$row"] = true; + } + } + + unset( $prefs[$name] ); + } + } // $value is ignored foreach ( $options as $key => $value ) { @@ -2383,6 +2402,8 @@ class User { $mapping[$key] = 'registered'; } elseif( isset( $multiselectOptions[$key] ) ) { $mapping[$key] = 'registered-multiselect'; + } elseif( isset( $checkmatrixOptions[$key] ) ) { + $mapping[$key] = 'registered-checkmatrix'; } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) { $mapping[$key] = 'userjs'; } else { @@ -2401,14 +2422,14 @@ class User { * and 'all', which forces a reset of *all* preferences and overrides everything else. * * @param $resetKinds array|string which kinds of preferences to reset. Defaults to - * array( 'registered', 'registered-multiselect', 'unused' ) - * for backwards-compatibility. + * array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ) + * for backwards-compatibility. * @param $context IContextSource|null context source used when $resetKinds - * does not contain 'all', passed to getOptionKinds(). - * Defaults to RequestContext::getMain() when null. + * does not contain 'all', passed to getOptionKinds(). + * Defaults to RequestContext::getMain() when null. */ public function resetOptions( - $resetKinds = array( 'registered', 'registered-multiselect', 'unused' ), + $resetKinds = array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ), IContextSource $context = null ) { $this->load(); diff --git a/skins/common/shared.css b/skins/common/shared.css index 1fe750e881..6e1c94fd36 100644 --- a/skins/common/shared.css +++ b/skins/common/shared.css @@ -232,6 +232,9 @@ td.mw-label { .prefsection table { width: 100%; } +.prefsection table.mw-htmlform-matrix { + width: auto; +} td.mw-submit { white-space: nowrap; } @@ -254,6 +257,11 @@ tr.mw-htmlform-vertical-label td.mw-label { white-space: nowrap; } +.mw-htmlform-matrix td { + padding-left: 0.5em; + padding-right: 0.5em; +} + input#wpSummary { width: 80%; margin-bottom: 1em; -- 2.20.1